package de.stefanocke.roo.kontaktliste.web.common;

import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.validation.constraints.NotNull;

import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.extensions.markup.html.form.DateTextField;
import org.apache.wicket.extensions.yui.calendar.DatePicker;
import org.apache.wicket.feedback.FeedbackMessage;
import org.apache.wicket.feedback.IFeedback;
import org.apache.wicket.feedback.IFeedbackMessageFilter;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.EnumChoiceRenderer;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.markup.html.form.IFormModelUpdateListener;
import org.apache.wicket.markup.html.form.NumberTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;

import de.stefanocke.roo.kontaktliste.web.common.AnnotationUtils.AnnotationMap;
import de.stefanocke.roo.wicket.RooFactoryMethod;
import de.stefanocke.roo.wicket.RooStereotype;

@RooStereotype("wicket.FormFields")
public abstract class FormFieldsPanel<T> extends Panel implements IFormModelUpdateListener {
	/***/
	private static final String CPM_CONTAINER_ID = "cpm";

	/***/
	private static final long serialVersionUID = 1L;

	/**
	 * The default ID for a FormFields component
	 */
	public static final String FORMFIELDS = "formfields";

	public FormFieldsPanel() {
		super(FORMFIELDS);
		init();
	}

	public FormFieldsPanel(IModel<T> model) {
		super(FORMFIELDS, model);
		init();
	}

	public FormFieldsPanel(String id) {
		super(id);
		init();
	}

	public FormFieldsPanel(String id, IModel<T> model) {
		super(id, model);
		init();
	}

	protected void init() {
		WebMarkupContainer cpmContainer = new WebMarkupContainer(CPM_CONTAINER_ID);
		List<Component> fields = createFields();
		cpmContainer.add(fields.toArray(new Component[0]));
		add(cpmContainer);

	}

	@Override
	protected void onInitialize() {
		super.onInitialize();
		Form<?> form = findParent(Form.class);
		if (form != null) {
			onInitForm(form);
		}
		// This is the first time, the CompoundPropertyModel of the parent is
		// available.
		// So, onModelChanged is called, to set the proper model for the form
		// fields.
		onModelChanged();
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void onModelChanged() {

		IModel<T> model = (IModel<T>) getDefaultModel();

		T modelObject = (T) getDefaultModelObject();
		if (modelObject == null) {
			// Create a new model which creates a new model object on load
			model = createNewModel();
		}
		get(CPM_CONTAINER_ID).setDefaultModel(new CompoundPropertyModel<T>(model));
		super.onModelChanged();
	}

	public FormComponent<?> getFormComponent(String path) {
		Component component = get(CPM_CONTAINER_ID).get(path);
		if (component != null && component instanceof SimpleFormField) {
			return ((SimpleFormField) component).getComponent();
		} else {
			throw new IllegalArgumentException("No form component found for path " + path);
		}
	}

	/**
	 * Allows for contributions to the parent form. F.e. adding form validators.
	 */
	protected void onInitForm(Form<?> form) {
	}

	/**
	 * Creates a new model for the embedded object. By default, it will
	 * reflectively create a new instance on load.
	 * 
	 * @param clazz the type of the model
	 * 
	 * @return the new model
	 */
	protected IModel<T> createNewModel() {

		return new LoadableDetachableModel<T>() {
			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected T load() {
				return createNewModelObject();
			}
		};

	}

	@SuppressWarnings("unchecked")
	@Override
	public void updateModel() {
		setDefaultModelObject((T) get(CPM_CONTAINER_ID).getDefaultModelObject());
	}

	protected T createNewModelObject() {

		if (getObjectClass() == null) {
			throw new IllegalArgumentException("ObjectClass is null. Please override getObjectClass().");
		}
		try {
			T newInstance = getObjectClass().newInstance();
			return newInstance;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	protected Class<T> getObjectClass() {
		return null;
	}

	/**
	 * Gets model
	 * 
	 * @return model
	 */
	@SuppressWarnings("unchecked")
	public final IModel<T> getModel() {
		return (IModel<T>) getDefaultModel();
	}

	/**
	 * Sets model
	 * 
	 * @param model
	 */
	public final void setModel(IModel<T> model) {
		setDefaultModel(model);
	}

	protected abstract List<Component> createFields();

	/** Interface for adding Buttons to a form field. */
	public interface ButtonContainer {
		RepeatingView getButtonsRepeater();
	}

	/**
	 * A Fragment holding a form component and its decorations.
	 * 
	 * @author stoc
	 * 
	 */
	public static abstract class SimpleFormField extends DecoratorFragment<FormComponent<?>> implements ButtonContainer {

		

		/***/
		private static final long serialVersionUID = 1L;

		/**
		 * The fragment providing the decorations for the form component.
		 */
		private static final String FRAGMENT_ID = "simpleFormField";

		/**
		 * 
		 * @param id the id. Will be used to bind the form component to the CPM.
		 * @param fragmentId the id of the form component fragment
		 * @param markupProvider the class, whose markup contains the fragment
		 *            markup for this fragment and the form component fragment.
		 * @param the annotations of the bean property (optional)
		 */
		public SimpleFormField(String id, String fragmentId, MarkupContainer markupProvider, AnnotationMap annotations) {
			super(id, FRAGMENT_ID, fragmentId, markupProvider);
			getOuterContainer().add(new RepeatingView("buttons"));

			getOuterContainer().add(new FeedbackPanel("feedback", new IFeedbackMessageFilter() {
				@Override
				public boolean accept(FeedbackMessage message) {
					// Due to WICKET-4544, we cannot use ComponentFeedbackPanel
					// and have to write this
					// weird filter.
					return getComponent().getFeedbackMessages().messages(null).contains(message);
				}
			}).setOutputMarkupId(true));

			// TODO: It might depend on the css framework, where to append this
			// behavior
			getOuterContainer().add(new ConditionalCssAttributeBehavior() {

				@Override
				protected boolean condition() {
					return !getComponent().isValid();
				}

				// TODO: This class might depend on the css framework...
				@Override
				protected String getCssClass() {
					return "error";
				}
			});
			if (annotations != null) {
				processAnnotations(annotations);
			}
			// TODO: Check if this works with all kind of form components. F.e.:
			// What about Radio Button Groups?
			addAjaxFormComponentBehaviors();
		}

		public SimpleFormField(String id, String fragmentId, MarkupContainer markupProvider) {
			this(id, fragmentId, markupProvider, null);
		}

		private void addAjaxFormComponentBehaviors() {
			FormComponent<?> component = getComponent();
			
			if(component.isRequired()){
				//Required fields are validated onBlur. But only until the user has done some input 				
				//or the field has been validated the first time.
				
				//TODO: Das ist alles recht gruslig. Die Bedingungen sollten besser auf Client-Seite evaluiert werden.
				component.add(new AjaxValidationBehavior("onBlur"){
					@Override
					protected void onUpdate(AjaxRequestTarget target) {
						replaceWithOnChange();
						super.onUpdate(target);
					}
					
					@Override
					protected void onError(AjaxRequestTarget target, RuntimeException e) {
						replaceWithOnChange();
						super.onError(target, e);
					}

					private void replaceWithOnChange() {
						getFormComponent().remove(this);
						getFormComponent().add(new AjaxValidationBehavior("onChange"));
					}
				});
			} else {
				component.add(new AjaxValidationBehavior("onChange"));
			}
		}

		protected void processAnnotations(AnnotationMap annotations) {
			if (annotations.contains(NotNull.class)) {
				getComponent().setRequired(true);
			}

		}

		@Override
		public RepeatingView getButtonsRepeater() {
			return (RepeatingView) getOuterContainer().get("buttons");
		}
		
		public FeedbackPanel getFeedbackPanel(){
			return (FeedbackPanel) getOuterContainer().get("feedback");
		}

	}
	
	private static class AjaxValidationBehavior extends AjaxFormComponentUpdatingBehavior {
		private AjaxValidationBehavior(String event) {
			super(event);
		}

		@Override
		protected void onUpdate(AjaxRequestTarget target) {
			addFeedbackPanels(target);
			// field validation successful. Look for form validators,
			// where this field is involved.

		}

		@Override
		protected void onError(AjaxRequestTarget target, RuntimeException e) {
			addFeedbackPanels(target);
		}
		
		@Override
		protected void onBind() {
			if(getSimpleFormField()==null){
				throw new IllegalArgumentException("AjaxValidationBehavior is only for FormComponents contained in SimpleFormField.");
			}
			super.onBind();
		}
		
		protected SimpleFormField getSimpleFormField(){
			return getComponent().findParent(SimpleFormField.class);
		}

		/**
		 * Adds feedback panels in the form to the ajax request target
		 * so they are updated.
		 * 
		 * @param target
		 */
		private void addFeedbackPanels(final AjaxRequestTarget target) {
			// Refresh the form field, feedback panel and decoration
			// TODO: This is quite a lot of html, especially for datetime fields. Can we improve that
			// by just refreshing the feedback panel and providing a java script that changes the css class
			// of the outer container?
			target.add(getSimpleFormField().getOuterContainer());  
			getComponent().findParent(Form.class).visitChildren(new IVisitor<Component, Void>() {
				@Override
				public void component(final Component component, final IVisit<Void> visit) {
					if (component instanceof SimpleFormField) {

						// target.add(((SimpleFormField)component).getOuterContainer());
						visit.dontGoDeeper();
					} else if (component instanceof IFeedback) {
						target.add(component);
						visit.dontGoDeeper();
					}
				}
			});
		}

	}

	/**
	 * A Fragment holding a complex form component and its decorations. A
	 * complex form component is one that consists of multiple other form
	 * components. It is usually some kind of panel.
	 * 
	 * @author stoc
	 * 
	 */
	@RooStereotype("wicket.ComplexFormField")
	public abstract static class ComplexFormField extends DecoratorFragment<Component> implements ButtonContainer {

		/***/
		private static final long serialVersionUID = 1L;

		/**
		 * The fragment providing the decorations for the form component.
		 */
		private static final String FRAGMENT_ID = "complexFormField";

		/**
		 * The fragment providing the decorations for the form component if it
		 * is in a list
		 */
		private static final String LIST_ITEM_FRAGMENT_ID = "complexListItem";

		/**
		 * 
		 * @param id the id. Will be used to bind the form component to the CPM.
		 * @param markupProvider the class, whose markup contains the fragment
		 *            markup for this fragment and the form component fragment.
		 * @param inList whether the complex form field appears as item of a
		 *            list or not
		 */
		public ComplexFormField(String id, MarkupContainer markupProvider, boolean inList) {
			super(id, inList ? LIST_ITEM_FRAGMENT_ID : FRAGMENT_ID, markupProvider);
			if (inList) {
				getOuterContainer().add(new RepeatingView("buttons"));
			}
		}

		/**
		 * 
		 * @param id the id. Will be used to bind the form component to the CPM.
		 * @param markupProvider the class, whose markup contains the fragment
		 *            markup for this fragment and the form component fragment.
		 */
		public ComplexFormField(String id, MarkupContainer markupProvider) {
			this(id, markupProvider, false);
		}

		@Override
		public RepeatingView getButtonsRepeater() {
			return (RepeatingView) getOuterContainer().get("buttons");
		}

		@Override
		@RooStereotype("wicket.createComponent")
		protected abstract Component createComponent(String id);

	}

	@RooStereotype("wicket.EnumSelect")
	public abstract static class EnumField<E extends Enum<E>> extends SimpleFormField {

		/***/
		private static final long serialVersionUID = 1L;

		/** ITD Workaround */
		public EnumField() {
			super(null, null, null);
		}

		public EnumField(String id, MarkupContainer markupProvider) {
			super(id, "select", markupProvider);
		}

		public EnumField(String id, IModel<E> model, MarkupContainer markupProvider) {
			super(id, "select", markupProvider);
			setDefaultModel(model); // TODO
		}

		@Override
		protected DropDownChoice<E> createComponent(String id) {
			return new DropDownChoice<E>(id, getChoices(), new EnumChoiceRenderer<E>(this));
		}

		protected List<? extends E> getChoices() {
			return Arrays.asList(getEnumValues());

		}

		protected abstract E[] getEnumValues();

	}

	@RooStereotype(value = "wicket.EntitySelect")
	public static abstract class SimpleEntitySelect<E> extends SimpleFormField {

		/***/
		private static final long serialVersionUID = 1L;

		/**
		 * This constructor is only a worakround for ITD based code generation.
		 * Don't use it!
		 */
		public SimpleEntitySelect() {
			super(null, null, null);
		}

		public SimpleEntitySelect(String id, MarkupContainer markupProvider) {
			super(id, "select", markupProvider);
		}

		public SimpleEntitySelect(String id, IModel<E> model, MarkupContainer markupProvider) {
			super(id, "select", markupProvider);
			setDefaultModel(model);
		}

		protected IModel<? extends List<? extends E>> createChoicesModel() {

			return new LoadableDetachableModel<List<? extends E>>() {

				private static final long serialVersionUID = 1L;

				@Override
				protected List<? extends E> load() {
					return loadChoices();
				}

			};
		}

		@Override
		protected DropDownChoice<E> createComponent(String id) {
			DropDownChoice<E> dropDownChoice = new DropDownChoice<E>(id, createChoicesModel(), createRenderer());
			return dropDownChoice;
		}

		protected abstract List<? extends E> loadChoices();

		protected IChoiceRenderer<? super E> createRenderer() {
			return new IChoiceRenderer<E>() {

				private static final long serialVersionUID = 1L;

				@Override
				public Object getDisplayValue(E entity) {
					return SimpleEntitySelect.this.getDisplayValue(entity);
				}

				@Override
				public String getIdValue(E entity, int index) {
					return SimpleEntitySelect.this.getIdValue(entity).toString();
				}
			};
		}

		protected Object getDisplayValue(E entity) {
			return getDefaultModelObjectAsString(entity);
		}

		protected abstract Object getIdValue(E entity);

	}

	@RooStereotype("wicket.SimpleListEditorField")
	public static abstract class SimpleListEditorField<T> extends ComplexFormField {

		private static final long serialVersionUID = 1L;

		public SimpleListEditorField(String id, MarkupContainer markupProvider) {
			super(id, markupProvider);
		}

		@Override
		protected ListEditorPanel<T> createComponent(String id) {
			return new ListEditorPanel<T>(id) {

				/***/
				private static final long serialVersionUID = 1L;

				@Override
				protected Component createItemComponent(String id) {
					return SimpleListEditorField.this.createItemComponent(id);
				}
			};
		}

		protected abstract Component createItemComponent(String id);

	}

	@RooStereotype("wicket.ComplexListEditorField")
	public static abstract class ComplexListEditorField<T> extends ComplexFormField {

		

		public ComplexListEditorField(String id, MarkupContainer markupProvider) {
			super(id, markupProvider);
			setOutputMarkupId(true);
		}

		@Override
		protected ListEditorPanel<T> createComponent(String id) {
			return new ListEditorPanel<T>(id) {
				/***/
				private static final long serialVersionUID = 1L;

				@Override
				protected Component createItemComponent(String id) {
					return ComplexListEditorField.this.createItemComponent(id);
				}

				@Override
				protected Component getAjaxTargetComponent() {
					return ComplexListEditorField.this;
				}

				@Override
				protected String getOrderPropertyExpression() {
					return ComplexListEditorField.this.getOrderPropertyExpression();
				}
			};
		}
		

		protected String getOrderPropertyExpression() {
			return null;
		}

		@SuppressWarnings("unchecked")
		@Override
		protected void onBeforeRender() {
			// No separate label, if there is at least one element in the list.
			boolean isEmpty = ((ListEditorPanel<T>) getComponent()).getList().isEmpty();
			getLabelComponent().setVisible(isEmpty);
			super.onBeforeRender();
		}

		protected abstract Component createItemComponent(String id);
	}

	@RooFactoryMethod(modelClasses = Boolean.class)
	protected SimpleFormField createCheckBox(String id) {
		return new SimpleFormField(id, "checkBox", this) {
			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected FormComponent<?> createComponent(String id) {
				return new CheckBox(id);
			}
		};
	}

	@RooFactoryMethod(modelClasses = { Date.class, Calendar.class })
	protected SimpleFormField createDateTimeField(String id, final Class<?> type, AnnotationMap annotationMap) {
		return new SimpleFormField(id, "dateTimeField", this, annotationMap) {

			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected FormComponent<?> createComponent(String id) {
				DateTextField dtf = new DateTextField(id);
				dtf.setType(type);
				dtf.add(new DatePicker());
				return dtf;
			}

		};
	}

	@RooFactoryMethod(modelClasses = Number.class)
	protected <N extends Number & Comparable<N>> SimpleFormField createNumberField(String id, final Class<N> type) {

		return new SimpleFormField(id, "numberField", this) {

			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected FormComponent<?> createComponent(String id) {
				NumberTextField<N> ntf = new NumberTextField<N>(id);
				ntf.setType(type);
				return ntf;
			}

		};
	}

	@RooFactoryMethod(modelClasses = String.class)
	protected SimpleFormField createTextField(String id, AnnotationMap annotationMap) {
		return new SimpleFormField(id, "textField", this, annotationMap) {

			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected FormComponent<?> createComponent(String id) {
				return new TextField<String>(id);
			}

		};
	}

	/**
	 * Fallback, if there are no more specific factory methods or components for
	 * the considered bean property. Creates a TextField an assumes the user to
	 * register an appropriate converter.
	 * 
	 * @param id the id
	 * @param type the type
	 * @return the TextFieldPanel
	 */
	@RooFactoryMethod
	protected <S> SimpleFormField createTextField(String id, final Class<S> type) {

		return new SimpleFormField(id, "textField", this) {
			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected FormComponent<?> createComponent(String id) {
				TextField<S> textField = new TextField<S>(id);
				textField.setType(type);
				return textField;
			}

		};
	}

}
